package com.ymk.base.mybatis;

import com.google.common.base.CaseFormat;
import com.google.gson.Gson;
import com.google.gson.internal.LinkedTreeMap;
import com.ymk.base.mybatis.utils.MyBatisHelper;
import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by laborc on 2018/12/8.
 */
@Intercepts({@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)})
public class ReturnStructPlugin implements Interceptor {
    private Configuration configuration;

    @Override
    public Object intercept(Invocation ivk) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) ivk.getArgs()[0];
        if (configuration == null) {
            this.configuration = mappedStatement.getConfiguration();
        }
        Object parameter = ivk.getArgs()[1];
        Map<String, Object> params = null;
        if (parameter == null) {
            return ivk.proceed();
        } else if (parameter instanceof Pagination) {
            params = ((Pagination) parameter).getParams();
        } else if (parameter instanceof Map) {
            params = ((Map<String, Object>) parameter);
        }
        if (params == null) {
            return ivk.proceed();
        } else {

            String returnStruct = (String) params.get("returnStruct");
            if (returnStruct != null) {
                Map returnStructMap = new Gson().fromJson(returnStruct, Map.class);
                BoundSql boundSql = mappedStatement.getBoundSql(params);
                String sql = boundSql.getSql();
                int selectCount = StringUtils.countMatches(sql, "select");
                if (selectCount == 1) {
                    int selectIndex = sql.indexOf("select");
                    int fromIndex = sql.indexOf("from");
                    String fromSql = sql.substring(fromIndex);

                    String fieldStr = sql.substring(selectIndex + 6, fromIndex);
                    String[] split = fieldStr.split(",");
                    String masterTableField = split[0].trim();
                    String masterTablePrefix = masterTableField.substring(0, masterTableField.length() - 1);

                    Set<String> returnFieldSet = new HashSet<>();

                    ResultMap resultMap = mappedStatement.getResultMaps().get(0);
                    ReturnStruct returnStructVO = new ReturnStruct();
                    returnStructVO.setResultMap(resultMap);
                    returnStructVO.setColumnPrefix(masterTablePrefix);
                    this.processStruct(returnStructMap, returnStructVO, returnFieldSet);

                    List<String> fieldList = Arrays.stream(split).map(String::trim).collect(Collectors.toList());
                    fieldList.remove(masterTableField);
                    for (String column : resultMap.getMappedColumns()) {
                        fieldList.add(masterTablePrefix + column.toLowerCase());
                    }
                    Iterator<String> iterator = fieldList.iterator();
                    while (iterator.hasNext()) {
                        String field = iterator.next();
                        String[] asSplit = field.split(" as ");
                        String compareField = null;
                        if (asSplit.length == 1) {
                            compareField = asSplit[0].trim();
                        } else if (asSplit.length == 2) {
                            compareField = asSplit[1].trim();
                        }
                        if (compareField != null && !returnFieldSet.contains(compareField)) {
                            iterator.remove();
                        }
                    }
                    String newSql = "select "+String.join(",", fieldList) + " " + fromSql;
                    BoundSql newBoundSql = MyBatisHelper.copyFromBoundSql(mappedStatement, boundSql, newSql);
                    ivk.getArgs()[0] = MyBatisHelper.copyFromMappedStatement(mappedStatement, new BoundSqlSource(newBoundSql));
                }
            }
            return ivk.proceed();
        }
    }

    private void processStruct(Map<String, Object> returnStructMap, ReturnStruct returnStruct, Set<String> returnFieldSet) {
        for (String property : returnStructMap.keySet()) {
            Object value = returnStructMap.get(property);
            if (value instanceof LinkedTreeMap) {
                LinkedTreeMap valueMap = (LinkedTreeMap) value;
                ReturnStruct childResultMap = this.getChildResultMap(property, returnStruct);
                if (childResultMap == null) {
                    continue;
                }
                this.processStruct(valueMap, childResultMap, returnFieldSet);
            } else {
                String filed = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, property);
                returnFieldSet.add(returnStruct.getColumnPrefix() + filed);
            }
        }
    }

    private ReturnStruct getChildResultMap(String property, ReturnStruct returnStruct) {
        List<ResultMapping> propertyResultMappings = returnStruct.getResultMap().getPropertyResultMappings();
        for (int i = propertyResultMappings.size() - 1; i >= 0; i--) {
            ResultMapping resultMapping = propertyResultMappings.get(i);
            if (property.equals(resultMapping.getProperty())) {
                ReturnStruct childReturnStruct = new ReturnStruct();
                if (returnStruct.getColumnPrefix().contains(".")) {
                    childReturnStruct.setColumnPrefix(resultMapping.getColumnPrefix());
                } else {
                    childReturnStruct.setColumnPrefix(returnStruct.getColumnPrefix() + resultMapping.getColumnPrefix());
                }
                ResultMap childResultMap = this.configuration.getResultMap(resultMapping.getNestedResultMapId());
                childReturnStruct.setResultMap(childResultMap);
                return childReturnStruct;
            }
        }
        return null;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private class ReturnStruct {
        private String columnPrefix;
        private ResultMap resultMap;

        public String getColumnPrefix() {
            return columnPrefix;
        }

        public void setColumnPrefix(String columnPrefix) {
            this.columnPrefix = columnPrefix;
        }

        public ResultMap getResultMap() {
            return resultMap;
        }

        public void setResultMap(ResultMap resultMap) {
            this.resultMap = resultMap;
        }
    }
}
